windows 系统调用

sky123

https://www.vergiliusproject.com/

R3 调用过程

这里以 OpenProcess 为例分析 Windows API 在 3 环部分的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
[User App 调用 OpenProcess]


kernel32.dll!OpenProcess
(导出 API 函数,实际上是个 IAT 跳板 thunk)


kernelbase.dll!OpenProcess
(真正构造参数、调用 Native API)


ntdll.dll!NtOpenProcess
(发起系统调用 syscall → Ring0)

Kernel32

Kernel32.dll 中,OpenProcess 属于导出名称,实际调用的函数是 OpenProcessStub

OpenProcessStub 函数并不直接实现核心功能,它只是一个导入表跳板(thunk)

1
2
3
4
5
6
7
8
9
HANDLE NTAPI OpenProcess(ACCESS_MASK dwDesiredAccess, BOOL bInheritHandle, HANDLE dwProcessId)
{
return __imp__OpenProcess@12(dwDesiredAccess, bInheritHandle, dwProcessId);
}

HANDLE NTAPI OpenProcessStub(ACCESS_MASK dwDesiredAccess, BOOL bInheritHandle, HANDLE dwProcessId)
{
return OpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId);
}

kernel32.dll 是 Windows API 的“用户态兼容壳”与“历史稳定接口”。这个 DLL 本身不执行系统调用逻辑,而是提供类似路由的功能,把老 API 调用映射到 kernelbase.dllntdll.dll 的新实现,为 Win32 应用提供熟悉且不变的函数导出。

在 IDA 的分析中我们发现 OpenProcessStub 最终调用的是 API-MS-Win-Core-Synch-L1-1-0.dll 中导出的 OpenProcess 函数。

1
2
3
4
.idata:77DE1960 ; Imports from API-MS-Win-Core-Synch-L1-1-0.dll
.idata:77DE1960 ;
.idata:77DE1960 ; HANDLE (__stdcall *OpenProcess)(ACCESS_MASK dwDesiredAccess, BOOL bInheritHandle, HANDLE dwProcessId)
.idata:77DE1960 extrn __imp__OpenProcess@12:dword

但是实际调试中我们发现,OpenProcessStub 调用的是 KernelBase.dll 中的 OpenProcess 函。

这实际上是 Windows 使用了一套 API‑set(虚拟 DLL)机制 来间接映射 API 调用。API‑set 是一种“协议/契约” DLL 名称,它不是实际存在的物理 DLL,而是一个 抽象接口。程序在编译时链接到这个虚拟 DLL,运行时 loader 会根据系统的 PEB.ApiSetSchema 表将其重定向到真正实现接口的模块(通常是 kernelbase.dllntdll.dll)。

KernelBase

kernelbase.dll 是 Windows 用户态 API 的核心实现模块,承载了大部分实际 Win32 API 的逻辑。

KernelBase.dll 中,OpenProcess 函数调用底层的 NtOpenProcess,并根据传入的参数设置好 OBJECT_ATTRIBUTESCLIENT_ID,返回目标进程的句柄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
HANDLE NTAPI OpenProcess(ACCESS_MASK dwDesiredAccess, BOOL bInheritHandle, HANDLE dwProcessId)
{
NTSTATUS status; // 用于存储 NtOpenProcess 的返回状态
HANDLE hProcess = NULL; // 输出的进程句柄
OBJECT_ATTRIBUTES ObjectAttributes; // 用于描述打开对象的属性
CLIENT_ID ClientId; // 用于指定目标进程 ID

// 设置目标进程 ID
ClientId.UniqueProcess = dwProcessId;
ClientId.UniqueThread = NULL;

// 初始化 OBJECT_ATTRIBUTES
InitializeObjectAttributes(
&ObjectAttributes,
NULL, // 没有对象名称(非命名对象)
bInheritHandle ? OBJ_INHERIT : 0, // 是否允许子进程继承句柄
NULL, // 根目录句柄为空
NULL // 不指定安全描述符
);

// 调用 Native API(ntdll!NtOpenProcess)进行系统调用
status = NtOpenProcess(&hProcess, dwDesiredAccess, &ObjectAttributes, &ClientId);

// 如果成功,则返回句柄
if (NT_SUCCESS(status))
return hProcess;

// 如果失败,设置 Win32 错误码(供 GetLastError 使用)
BaseSetLastNTError(status);
return NULL;
}

Ntdll

Ntdll.dll 中,ZwOpenProcess 函数被导出为 NtOpenProcessZwOpenProcess 函数。

Nt*Zw* 是微软 Windows 操作系统中用于表示Native API(原生系统调用接口)的两套函数前缀。

内核中,系统本身(驱动、内核模块)有时候需要访问用户态受保护的资源(比如任意进程的 handle、文件对象等)。

  • 如果它还走 Nt 版本,会强制执行 ObCheckObjectAccess(安全访问检查);
  • Zw* 的设计就是告诉内核:“我知道我在干啥,我是受信任代码,我不需要再检查 DACL/SACL 了。”

而因为用户态不能自行决定“跳过权限检查”,所以 NtZw 完全等价,但是为例历史兼容性,Windows 还是保留了两种前缀的函数导出名称。

ntdll.dll!ZwOpenProcess 是真正发起系统调用进 0 环的函数,该函数的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; ===========================================================
; 函数: ZwOpenProcess
; 原型: NTSTATUS NTAPI ZwOpenProcess(
; _Out_ PHANDLE ProcessHandle,
; _In_ ACCESS_MASK DesiredAccess,
; _In_ POBJECT_ATTRIBUTES ObjectAttributes,
; _In_opt_ PCLIENT_ID ClientId);
;
; 说明:
; - 位于 ntdll.dll 中
; - 是用户态系统调用封装(syscall stub)
; - 最终通过 SYSENTER 进入内核 NtOpenProcess 实现
; ===========================================================

_ZwOpenProcess@16 PROC
mov eax, 0BEh ; 设置 syscall 编号,0xBE = NtOpenProcess
mov edx, [0x7FFE0300h] ; KUSER_SHARED_DATA 中的 SystemCall 地址
call dword ptr [edx] ; 间接调用,触发 sysenter 进入内核
retn 10h ; stdcall 调用约定:清理 4 个参数(4×4=16 字节)
_ZwOpenProcess@16 ENDP
  • 首先这里 eax 寄存器用于传递系统调用号(SSDT 索引);0xBE 表示 NtOpenProcess 的系统调用号,供内核用来在 SSDT 表中查找实际实现。

  • 0x7FFE0300 是 Windows 的 KUSER_SHARED_DATA 结构中的 SystemCall 字段,该字段默认指向 KiFastSystemCall

    KUSER_SHARED_DATA 是 Windows 中用于优化性能、实现 syscall 跳板等功能的结构。Windows 将该结构所在的物理页映射到每个 3 环进程地址空间以及 0 环地址空间固定地址

    • 对于用户态这个结构位于所有进程的 0x7FFE0000 地址处。
    • 对于内核态这个结构位于 0xFFDF0000/0xFFFFF78000000000 地址处。

KiFastSystemCall(SYSENTER)

KiFastSystemCall 函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
; ===========================================================
; 函数: KiFastSystemCall
; 原型: void __stdcall KiFastSystemCall(void);
;
; 说明:
; - 用户态系统调用入口封装
; - 被 Zw/Nt* 函数统一调用
; - 封装 sysenter 指令的调用格式
; ===========================================================

_KiFastSystemCall@0 PROC
mov edx, esp ; 将当前用户栈指针传递给 edx(sysenter 需要)
sysenter ; Intel 快速系统调用指令 → 切入内核态
_KiFastSystemCall@0 ENDP

; ===========================================================
; 函数: KiFastSystemCallRet
; 原型: void __stdcall KiFastSystemCallRet(void);
;
; 说明:
; - 系统调用返回点
; - sysexit 返回后,执行此 ret 恢复调用者上下文
; - 一般由内核代码设定 eip 返回到这里
; ===========================================================

_KiFastSystemCallRet@0 PROC
retn ; stdcall 返回,恢复用户代码执行
_KiFastSystemCallRet@0 ENDP

这个函数本质上就是执行 sysenter 指令进内核然后返回。

  • sysenter 指令本身 不保存上下文,也不自动切换用户栈到内核栈,它依赖于寄存器中携带的参数来完成调用,其中 EDX 传入的是用户态栈地址,供内核作为参数或上下文恢复使用。
  • KiFastSystemCallKiFastSystemCallRet 两个函数实际上是一个整体。也就是说执行完 sysenter 指令后紧接着就会执行 KiFastSystemCallRetretn 指令返回。

KiIntSystemCall(INT 2E)

_KiIntSystemCall 使用的是 INT 2E 并且 EDX 指向的是参数地址而不是栈顶。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
; ------------------------------------------------------------------------
; 函数原型(stdcall 调用约定):
; DWORD __stdcall KiIntSystemCall(void);
; 描述:
; 使用 INT 2Eh 中断机制进行系统调用封装。
; EAX 需事先设置为系统调用号,EDX 自动设置为参数表地址。
; ------------------------------------------------------------------------

PUBLIC _KiIntSystemCall@0
_KiIntSystemCall@0 PROC

; ESP + 4 是返回地址
; ESP + 8 才是第一个真正的参数地址
; 因为本函数是 __stdcall,无参数,但实际系统调用参数在 ESP+8 开始

lea edx, [esp + 8] ; 将用户态参数表的起始地址传入 EDX(供内核使用)

int 2Eh ; 触发中断 2Eh,进入内核,跳转到 IDT[2Eh],由 _KiSystemService 处理

retn ; 返回调用者(不需要额外清理堆栈)

_KiIntSystemCall@0 ENDP

系统调用指令

出于性能优化、兼容性和安全性考虑,Windows 在不同版本和架构中采用了不同的系统调用机制

系统调用方式 用户态调用封装 内核入口函数 处理器架构 用途 / 备注
int 2Eh _KiIntSystemCall _KiSystemService x86 旧机制,Win9x ~ Windows XP 初期使用
sysenter _KiFastSystemCall _KiFastCallEntry x86 Windows XP 引入的快速调用机制,默认方式
syscall 内联指令(无显式封装) _KiSystemCall64 x64 Windows x64 架构的系统调用标准入口(如 Nt 函数内联)

SYSENTER/SYSEXIT

SYSENTER 是一种 快速系统调用机制,由 Intel 在 P6(Pentium Pro)架构中引入,用于从用户态(Ring3)切换到内核态(Ring0),替代传统的 int 0x2E 中断方式,以减少中断门 IDT 的开销。

SYSENTER

SYSENTER 的行为由以下三个 MSR 控制,CPU 不会自动保存用户态上下文:

MSR 名称 编号(十六进制) 内容说明
IA32_SYSENTER_CS 0x174 内核模式的代码段选择子(CS)
IA32_SYSENTER_ESP 0x175 内核模式栈顶指针(ESP)
IA32_SYSENTER_EIP 0x176 入口函数地址(EIP)

MSR(Model-Specific Register,模型特定寄存器)是由 CPU 厂商(如 Intel、AMD)定义的一类特殊寄存器,用于控制和监视处理器的底层功能。这些寄存器的功能高度依赖于处理器型号和架构,因此被称为“模型特定”。

MSR(Model-Specific Register)虽然是寄存器,但不像 EAX/CR0 那样有固定名字,它们通过编号(索引号)访问,这些编号是由 Intel/AMD 在官方手册中定义的。例如 IA32_SYSENTER_CS(0x174)SYSENTER 设置的 CS 代码段。

Intel 提供了 RDMSR/WRMSR 用来读写 MSR 寄存器,编号通过 ECX 传递,读/写 64 位的值为 EDX:EAX(高32位在 EDX,低32位在 EAX)。

另外在 WinDbg 可以通过 .rdmsr/.wrmsr 命令来读写 MSR 寄存器。

  • .rdmsr NNN:读取指定编号的 MSR(NNN 是十六进制)

    1
    2
    0: kd> .rdmsr 174
    msr[174] = 00000000`00000008
  • .wrmsr NNN VALUE:向 MSR 写入值(注意不要误操作系统 MSR)

    1
    0: kd> .wrmsr 174 00000000`00000010

在执行 SYSENTER 指令的过程中,硬件会自动完成如下操作:

  1. EIP ← IA32_SYSENTER_EIP :跳转到内核中指定的入口函数(KiFastCallEntry)。

  2. CS ← IA32_SYSENTER_CS & 0xFFFC :设置代码段寄存器为 Ring 0 代码段选择子。

  3. SS ← IA32_SYSENTER_CS + 8 :设置堆栈段寄存器,必须满足 GDT 中段排列规范( 要求 GDT 中段排列满足内核代码段内核数据段紧邻)。

  4. ESP ← IA32_SYSENTER_ESP :切换到内核堆栈(栈顶指针)。

    注意

    IA32_SYSENTER_ESP全局的、CPU层面的默认值。它 不是线程级别的,不能区分哪个线程用哪个内核栈,所以必须用线程上下文中的 TSS.Esp0 重新切换到线程专属栈

  5. 当前特权级从 CPL=3 切换为 CPL=0 :切换到 Ring 0,进入内核模式。

  6. EFLAGS.VM / IF 标志位被清除退出虚拟 8086 模式关闭中断

    注意

    • 由于没有自动保存 EFLAGS,用户态想要恢复中断、TF 状态等,需要内核中手动保存和恢复。

    • IF(bit 9)控制 是否允许中断(即是否响应外部硬件中断)。如果中断在进入内核后一开始就触发,可能 打断尚未完成栈切换、TrapFrame 建立、上下文保存 的早期内核初始化逻辑,这会导致系统崩溃(比如使用未初始化栈空间、破坏返回地址等)。所以 进入内核后的第一件事就是关闭中断,等内核准备好了才通过 sti 指令开启中断。

SYSEXIT

SYSEXIT 指令用于快速返回到特权级为 3 的用户代码。它是 SYSENTER 指令的配套指令。该指令被优化以在从特权级 0 的系统过程返回到特权级 3 的用户过程时提供最大性能。此指令必须在特权级 0 的代码中执行

在执行 SYSEXIT 指令的过程中,硬件会自动完成如下操作:

  1. CS ← IA32_SYSENTER_CS + 16(代码段) :从 MSR IA32_SYSENTER_CS 中的值,加上 16(表示 Ring 3 的代码段描述符)后写入 CS
  2. EIP ← EDX :将 EDX 的值作为返回到用户态的代码入口。
  3. SS ← IA32_SYSENTER_CS + 24(堆栈段) :同样基于 IA32_SYSENTER_CS,加上 24 计算 Ring3 的 SS 段选择子。
  4. ESP ← ECX :将 ECX 的值作为用户态堆栈的 ESP。
  5. CPL ← 3 :当前特权级设置为 3,即从 Ring 0 切换到 Ring 3。

INT 2E/IRETD

这是早期 Windows(如 Windows NT4 ~ XP)采用的经典系统调用方式。

INT 2E

中断指令 INT 的功能相对于 SYSENTER 有一些不同:

  1. CS:EIP ← IDT[0x2E] :跳转到内核中指定的入口函数(_KiSystemService)。
  2. SS← TSS.SS0 :加载内核模式的堆栈段(通常为 0x10)。
  3. ESP ← TSS.ESP0 :切换到内核堆栈(栈顶指针)。
  4. 内核堆栈中依次压入用户模式的寄存器 SSESPEFLAGSCSEIPINT 后的地址)。
  5. 当前特权级从 CPL=3 切换为 CPL=0 :切换到 Ring 0,进入内核模式。
  6. EFLAGS.IF / TF / NT / VM 标志位被清除 :关闭中断、退出单步调试与嵌套任务、强制退出虚拟 8086 模式。
    • IF(Interrupt Flag) 被清除:防止系统调用早期被外部中断打断,避免栈未初始化就进入中断处理程序。
    • TF(Trap Flag) 被清除:禁止单步调试陷阱,防止调试器干扰内核指令执行。
    • NT(Nested Task) 被清除:避免因 IRETD 触发任务切换(Task Gate),保持当前任务上下文。
    • VM(Virtual 8086 Mode) 被清除:退出虚拟 8086 模式,进入真实的保护模式 Ring 0。

IRETD

从栈中弹出 EIPCSEFLAGSESPSS 寄存器返回用户态。

SYSCALL / SYSRET(仅限 x64)

AMD 最先提出,Intel 后跟进,在 x64 架构中标准化。

R0 调用过程

KiFastCallEntry(SYSENTER)

清理段寄存器

因为 SYSENTER 不会设置 DS/ES/FS/GS 段寄存器,而这些寄存器在内核中仍会被用到,如果它们值非法或残留用户态设置,可能导致内核访问错误或崩溃。所以 Windows 进入内核后第一件事就是清理或初始化这些段寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUBLIC _KiFastCallEntry
_KiFastCallEntry proc

; =============================================================================
; 初始化段寄存器:
; - 设置 DS/ES 为用户数据段(选择子 0x23 = 0x20 | 3)
; - 设置 FS 为 KPCR(处理器控制块,选择子 0x30)
; =============================================================================

mov ecx, KGDT_R3_DATA OR RPL_MASK ; ecx = 用户数据段选择子 (0x20 | 0x3 = 0x23)
push KGDT_R0_PCR ; KPCR 段选择子压栈 (0x30)
pop fs ; 设置 FS -> KPCR(fs:[0] = KPCR.SelfPcr)
mov ds, ecx ; DS = 0x23,用户数据段
mov es, ecx ; ES = 0x23,用户数据段

切换内核栈

SYSENTER 之后,CPU 处于 Ring 0,且中断被关闭,因此在当前核上执行的 DPC 栈代码是 线程安全 的 —— 不会被打断,也不会与别的线程同时访问同一个栈。

DPC 栈(Deferred Procedure Call Stack)是 Windows 操作系统中为每个处理器(CPU)分配的一块专用 内核栈空间,用于处理系统调用初期、延迟任务(DPC)、中断服务后的下半部分等非线程上下文的代码执行。

但是 DPC 栈不是为线程准备的栈,不能承载线程级的数据结构(例如 KTRAP_FRAME),并且后续的系统调用处理必须允许“线程切换”。因此这里需要将堆栈切换为 TSS.Esp0

1
2
3
4
5
6
; =============================================================================
; 切换栈指针为当前线程的内核栈顶(TSS.Esp0)
; SYSENTER 设置的 ESP 可能是通用的 DPC 栈,此处替换为线程专属栈
; =============================================================================
mov ecx, PCR[PcTss] ; 获取当前 CPU 的 TSS 结构地址
mov esp, [ecx]+TssEsp0 ; 设置 ESP = 当前线程的内核栈顶

这里实际上是从 fs 对应的 KPCRTSS 来定位线程的 TSS 结构地址。

1
2
mov     ecx, large fs:_KPCR.TSS
mov esp, [ecx+_KTSS.Esp0]

保存 TRAP_FRAME

接下来 KiFastCallEntry 会在内核线程栈中构造 KTRAP_FRAME 结构,用于建立完整的“系统调用返回环境”,确保系统调用在内核态运行时能保持线程上下文、支持调试、允许中断、并准备好未来的调度。

其中 KTRAP_FRAME 结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct _KTRAP_FRAME
{
// 调试器使用的辅助信息(DbgK 命令、栈回溯)
ULONG DbgEbp; // 0x00 上层 EBP
ULONG DbgEip; // 0x04 返回地址(EIP)
ULONG DbgArgMark; // 0x08 特定标志 0xBADB0D00
ULONG DbgArgPointer; // 0x0C 指向用户参数的位置

// 临时段寄存器和堆栈
USHORT TempSegCs; // 0x10 临时的用户代码段(CS)
UCHAR Logging; // 0x12 日志标志(调试器使用)
UCHAR Reserved; // 0x13 保留字段
ULONG TempEsp; // 0x14 临时保存的 ESP(用户堆栈)

// 调试寄存器(如果调试启用)
ULONG Dr0; // 0x18
ULONG Dr1; // 0x1C
ULONG Dr2; // 0x20
ULONG Dr3; // 0x24
ULONG Dr6; // 0x28
ULONG Dr7; // 0x2C

// 段寄存器
ULONG SegGs; // 0x30
ULONG SegEs; // 0x34
ULONG SegDs; // 0x38

// 通用寄存器(部分易失)
ULONG Edx; // 0x3C
ULONG Ecx; // 0x40
ULONG Eax; // 0x44

// 系统调用(或中断/异常)发生之前 CPU 处于的特权级(如用户态 CPL=3 时值为 1)
ULONG PreviousPreviousMode; // 0x48

// 异常处理链(SEH)指针,位于 TEB 中
struct _EXCEPTION_REGISTRATION_RECORD* ExceptionList; // 0x4C

// 线程环境块(TEB)段
ULONG SegFs; // 0x50

// 通用寄存器(非易失性)
ULONG Edi; // 0x54
ULONG Esi; // 0x58
ULONG Ebx; // 0x5C
ULONG Ebp; // 0x60

// 错误码(如中断错误码)
ULONG ErrCode; // 0x64

// 控制流上下文
ULONG Eip; // 0x68
ULONG SegCs; // 0x6C
ULONG EFlags; // 0x70

// 堆栈上下文
ULONG HardwareEsp; // 0x74
ULONG HardwareSegSs; // 0x78

// 以下是 Virtual 8086 模式下的段寄存器(一般为 0)
ULONG V86Es; // 0x7C 👈 TSS.Esp0
ULONG V86Ds; // 0x80
ULONG V86Fs; // 0x84
ULONG V86Gs; // 0x88
};

TSS.Esp0 在初始状态下指向 V86Es,因此需要从 SS 寄存器开始构造。

首先依次压入 SSESP

1
2
push    KGDT_R3_DATA OR RPL_MASK       ; 压栈用户 SS(0x23)
push edx ; 压栈 ESP(KiFastSystemCall 中保存的用户态 ESP)
  • 用户态的 SS 是固定的 0x23,因此即使在用户态修改了 SS 的值,经过系统调用后也会自动修复。
  • ESP 来自 KiFastSystemCall 函数保存到 EDX 的用户态栈顶。

关于 EFLAGS 寄存器这里要做一些特殊处理。除此之外还顺便将指向用户态栈顶EDX 修改为指向用户态参数

1
2
3
4
5
6
7
8
pushfd                                 ; 压栈当前 EFLAGS(注意:此时是内核模式下已被 SYSENTER 修改过的,IF=0、VM=0)

Kfsc10:
push 2 ; 构造一个干净的 EFLAGS,只有 bit1=1(其他清零)
add edx, 8 ; EDX 指向用户态参数 (跳过KiFastSystemCall 和 ZwOpenProcess 两个函数的返回地址)
popfd ; 用 2 替换当前 EFLAGS(清除 IF、DF、TF、NT)

or byte ptr [esp+1], EFLAGS_INTERRUPT_MASK/0100h ; 把 R3 下的可屏蔽中断打开 (EFLAGS 寄存器第 10 位 IF=1)
  • 对于内核态的 EFLAGS:通过 push 2popfd 设置为干净状态,只保留 bit1,关闭中断(IF=0),清除 DF/TF/NT

    EFLAGS 寄存器的 bit 1 是一个保留位始终为 1,不可清除。

  • 对于用户态的 EFLAGS:通过修改 pushfd 压栈副本中的 bit10,确保将来返回用户态时中断是打开的(IF=1)。

之后保存 CSEIP

1
2
push    KGDT_R3_CODE OR RPL_MASK       ; 压栈用户 CS(0x1B)
push dword ptr ds:[USER_SHARED_DATA+UsSystemCallReturn] ; 压栈用户 EIP 为 KiFastSystemCallRet
  • CS 是和 SS 一样是固定的值,这里设置为 0x1B。
  • EIP 设置为 USER_SHARED_DATA.SystemCallReturn,这里默认是 KiFastSystemCallRet 函数。

接下来填充 KTRAP_FRAMEErrCode 字段为 0,表示无异常。

1
push    0                               ; 压栈填充 ErrCode 用于错误处理

接下来是一些非易失寄存器,这里直接保存即可。

1
2
3
4
push    ebp                             ; 保存非易失寄存器 ebp
push ebx ; 保存非易失寄存器 ebx
push esi ; 保存非易失寄存器 esi
push edi ; 保存非易失寄存器 edi

接下来会把 EBXESI 两个寄存器分别指向两个重要的结构。另外这里还会保存用户态的 FS(0x3B),同样是固定的值。

1
2
3
mov     ebx, PCR[PcSelfPcr]            ; 获取当前处理器 KPCR 地址
push KGDT_R3_TEB OR RPL_MASK ; 压栈用户模式 FS(0x3B) 指向 TEB
mov esi, [ebx].PcPrcbData+PbCurrentThread ; 获取当前线程的地址
  • EBX 指向当前处理器的 KPCR 结构。由于 KPCR 结构体的地址位于内核 FS(0x30) 对应段描述符的基址中,因此这里是借助 KPCRSelfPcr 字段获取的 KPCR 地址。

    1
    mov ebx, large fs:_KPCR.SelfPcr
  • ESI 指向当前线程对应的 KTHREAD 结构。这个结构是通过 KPCR.PrcbData.CurrentThread 获得的。

    1
    mov esi, [ebx+_KPCR.PrcbData.CurrentThread]

为了防止内核执行期间访问用户态的 SEH(结构化异常处理)链,避免异常处理被用户控制的数据干扰或利用。接下来会保存用户态的异常链地址,同时清空当前线程的异常处理链,防止内核期间意外触发结构化异常时查表失败或被恶意利用。

1
2
push    [ebx].PcExceptionList          ; 保存旧的异常链
mov [ebx].PcExceptionList, EXCEPTION_CHAIN_END ; 设置新的空链 EXCEPTION_CHAIN_END(-1)

这里 EBX 就是前面获取的 KPCR 结构地址:

1
2
push    [ebx+_KPCR.NtTib.ExceptionList]		; TRAP_FRAME.ExceptionList = KPCR.NtTib.ExceptionList
mov [ebx+_KPCR.NtTib.ExceptionList], -1 ; KPCR.NtTib.ExceptionList = -1

之后会为 KTRAP_FRAME 分配剩余空间并作地址检查。

1
2
3
4
5
6
7
8
9
10
; 保存之前的模式(用户模式),并为 trap frame 分配剩余空间
mov ebp, [esi].ThInitialStack ; 获取线程的内核栈顶(初始栈地址)
push MODE_MASK ; 将调用来源(用户模式 1)压入栈中
sub esp, TsPreviousPreviousMode ; 为 trap frame 分配剩余空间,TsPreviousPreviousMode(0x48) 是 PreviousPreviousMode 在 KTRAP_FRAME 的偏移
sub ebp, NPX_FRAME_LENGTH + KTRAP_FRAME_LENGTH ; 0x210 + 0x8C = 0x29C
mov byte ptr [esi].ThPreviousMode, MODE_MASK ; 设置线程的 PreviousMode = UserMode

; 校验 TrapFrame 构造完整性,如果栈异常则拒绝执行
cmp ebp, esp
jne short Kfsc91
  • 设置 KTRAP_FRAME.PreviousPreviousModeKPCR.PrcbData.CurrentThread->PreviousMode 为 1 表示是来自用户态的调用。
  • 比较 KTRAP_FRAME 地址与 KPCR.PrcbData.CurrentThread->InitialStack - sizeof(NPX_FRAME) - sizeof(KTRAP_FRAME),如果不相等则拒绝执行。
    • KTRAP_FRAME 地址是在当 ESP 指向 KTRAP_FRAME.PreviousPreviousMode 的时候有减去 PreviousPreviousModeKTRAP_FRAME 的偏移得到的。
    • KTHREAD.InitialStack 指向内核栈底,而在内核栈底保存着 NPX_FRAMEKTRAP_FRAME 两个结构。
    • 如果两者不相等说明 TSS.Esp0 并没有指向 KTRAP_FRAME.V86Es 位置,即当前线程是以 虚拟8086(V86)模式 创建的线程,具有特殊的栈结构。或者栈指针被破坏、异常栈使用错误。

Kfsc91 会抛出 #UD(Invalid Opcode,非法/未定义指令)异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;
; 如果 sysenter 指令是在非法的用户上下文(如 16 位段或虚拟 8086 模式)中执行的,
; 则无法正确返回用户态代码,也无法安全构造 TrapFrame 进行系统调用处理。
; 因此,模拟构造一个伪造的“用户模式”上下文,然后跳转到非法指令异常处理函数 _KiTrap06。
;

Kfsc90:
mov ecx, PCR[PcTss] ; 获取当前处理器的 TSS 指针
mov esp, [ecx]+TssEsp0 ; 使用 TSS.ESP0 作为新的内核栈顶(当前线程的内核栈)

push 0 ; 模拟保存虚拟 8086 模式下的 ES
push 0 ; 模拟保存虚拟 8086 模式下的 DS
push 0 ; 模拟保存虚拟 8086 模式下的 FS
push 0 ; 模拟保存虚拟 8086 模式下的 GS

push KGDT_R3_DATA OR RPL_MASK ; 用户数据段 SS(0x20 | 0x3 = 0x23)
push 0 ; 用户栈指针 ESP(由于无法得知,设置为 0 占位)
push EFLAGS_INTERRUPT_MASK + EFLAGS_V86_MASK + 2h
; 构造 EFLAGS 值,IF=1(允许中断),VM=1(虚拟8086模式),bit1=1(系统要求)
push KGDT_R3_CODE OR RPL_MASK ; 用户代码段 CS(0x18 | 0x3 = 0x1B)
push 0 ; 用户 EIP(指令指针,未知,设为 0)

jmp _KiTrap06 ; 跳转到非法指令处理函数,相当于抛出 #UD(Invalid Opcode 异常)

;
; 如果 Trap Frame 构造时栈指针不一致(说明栈异常或非法线程调用),则跳转处理异常。
;

Kfsc91:
jmp Kfsc90 ; 回退到异常处理逻辑

最后再初始化 KTRAP_FRAME 中调试相关字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;
; 设置当前线程的 Trap Frame 地址,并处理调试相关逻辑
;

and dword ptr [ebp].TsDr7, 0 ; 清除 Trap Frame 中的 DR7(调试寄存器)值,防止继承用户态调试状态
test byte ptr [esi].ThDebugActive, 0ffh ; 检查当前线程是否启用了调试(ThDebugActive != 0)
mov [esi].ThTrapFrame, ebp ; 将当前 Trap Frame 的地址写入线程结构中的 ThTrapFrame 字段

jnz Dr_FastCallDrSave ; 如果线程启用了调试功能,则跳转保存调试寄存器(如 DR0~DR6)

Dr_FastCallDrReturn:

SET_DEBUG_DATA ; 设置调试器辅助信息字段(DbgEbp / DbgEip / ArgPointer 等)
; 注意:该宏体内会破坏 ebx 和 edi 的值

sti ; 恢复中断标志位(IF=1),允许内核再次响应该核上的中断请求

?FpoValue = 0 ; 表示当前栈帧未省略帧指针(ebp 保留),供调试器分析使用

Dr_FastCallDrSave 函数会保存用户态的调试寄存器到 KTRAP_FRAME 结构,然后从 KPRCB.ProcessorState.SpecialRegisters 取出调试寄存器对应内核态的值赋值给调试寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
; =============================================================================
; 函数:Dr_FastCallDrSave
; 描述:若线程启用了调试功能(ThDebugActive != 0),则保存用户态调试寄存器,
; 并清除 DR7,防止用户设置的硬件断点影响内核,再恢复内核态默认值。
; 支持 TrapFrame 构造过程。
; =============================================================================
Dr_FastCallDrSave proc near ; ← _KiFastCallEntry 调用处跳转

; ----------------------------------------------------------------------------
; 检查是否处于虚拟 8086 模式(EFLAGS.VM = 1),若是则跳转保存调试寄存器
; ----------------------------------------------------------------------------
test [ebp+_KTRAP_FRAME.EFlags], EFLAGS_V86_MASK ; = 0x20000
jnz short Dr_SaveDebugRegs

; ----------------------------------------------------------------------------
; 检查代码段最低位(CPL=3)是否为用户态调用,若不是则直接返回
; ----------------------------------------------------------------------------
test byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
jz Dr_FastCallDrReturn

; ----------------------------------------------------------------------------
; 保存调试寄存器内容到 TrapFrame(用户态调试上下文)
; ----------------------------------------------------------------------------
Dr_SaveDebugRegs:
mov ebx, dr0
mov ecx, dr1
mov edi, dr2
mov [ebp+_KTRAP_FRAME.Dr0], ebx
mov [ebp+_KTRAP_FRAME.Dr1], ecx
mov [ebp+_KTRAP_FRAME.Dr2], edi

mov ebx, dr3
mov ecx, dr6
mov edi, dr7
mov [ebp+_KTRAP_FRAME.Dr3], ebx
mov [ebp+_KTRAP_FRAME.Dr6], ecx
xor ebx, ebx
mov [ebp+_KTRAP_FRAME.Dr7], edi

; 清除当前 DR7,避免用户态设置影响内核调试状态
mov dr7, ebx

; ----------------------------------------------------------------------------
; 恢复内核态调试寄存器值(来自 KPCR.Prcb.ProcessorState.SpecialRegisters)
; ----------------------------------------------------------------------------
mov edi, large fs:_KPCR.Prcb

mov ebx, [edi+_KPRCB.ProcessorState.SpecialRegisters.KernelDr0]
mov ecx, [edi+_KPRCB.ProcessorState.SpecialRegisters.KernelDr1]
mov dr0, ebx
mov dr1, ecx

mov ebx, [edi+_KPRCB.ProcessorState.SpecialRegisters.KernelDr2]
mov ecx, [edi+_KPRCB.ProcessorState.SpecialRegisters.KernelDr3]
mov dr2, ebx
mov dr3, ecx

mov ebx, [edi+_KPRCB.ProcessorState.SpecialRegisters.KernelDr6]
mov ecx, [edi+_KPRCB.ProcessorState.SpecialRegisters.KernelDr7]
mov dr6, ebx
mov dr7, ecx

; ----------------------------------------------------------------------------
; 跳转回常规流程,继续执行系统调用处理逻辑
; ----------------------------------------------------------------------------
jmp Dr_FastCallDrReturn

Dr_FastCallDrSave endp

SET_DEBUG_DATA 宏则会保存 EIPEBP,和参数地址信息,方便我们调试的时候进行栈 0 环到 3 环的回溯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
;++
;
; 从多个寄存器或变量中提取数据并写入 TrapFrame 的调试辅助字段。
; 目的是方便调试器(如 WinDbg 的 KB 命令)追踪系统调用的参数与栈信息。
; 特别适用于从内核态返回用户态的路径上(系统调用、异常、或中断处理)。
;
; 本宏会破坏 ebx 和 edi 的值。
;--

SET_DEBUG_DATA macro

ife FPO ; 如果未启用帧指针省略(Frame Pointer Omission)

;
; 本宏由 ENTER_SYSCALL、ENTER_TRAP 和 ENTER_INTERRUPT 等入口宏调用,
; 用于设置 TrapFrame 的调试字段(DbgEbp、DbgEip、DbgArgPointer 等)。
; 调试器可以通过这些字段还原调用栈和参数信息。
;

mov ebx, [ebp]+TsEbp ; 提取当前帧中的 EBP(调用者帧指针),保存到 ebx
mov edi, [ebp]+TsEip ; 提取当前帧中的 EIP(返回地址),保存到 edi
mov [ebp]+TsDbgArgPointer, edx ; 将 edx(指向系统调用参数)保存到 TrapFrame 的 DbgArgPointer 字段
mov [ebp]+TsDbgArgMark, 0BADB0D00h ; 设置调试标记,用于调试器识别系统调用帧(0xBADB0D00 是魔数)
mov [ebp]+TsDbgEbp, ebx ; 把 EBP 保存到 TrapFrame 的 DbgEbp 字段
mov [ebp]+TsDbgEip, edi ; 把 EIP 保存到 TrapFrame 的 DbgEip 字段

endif

endm

调用 SSDT 表函数

系统服务分发表(SSDT,System Service Dispatch Table)是 Windows 内核中的一个关键数据结构,用来将用户空间的系统调用映射到对应的内核函数。

当用户程序通过 int 0x2e(老版)或 sysenter/syscall(新版) 发起系统调用时:

  • 系统调用号(Service ID)被放入 EAX
  • 内核通过 SSDT[EAX] 查找到该号对应的内核函数指针并调用它。

SSDT 实际是一个结构体数组,每一项描述一个服务表(例如 Native API、Win32k GUI API等)。其核心结构如下:

1
2
3
4
5
6
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PULONG ServiceTableBase; // SSDT 函数地址表
PULONG ServiceCounterTableBase; // 服务调用计数表(调试用途)
ULONG NumberOfServices; // 系统服务总数
PUCHAR ParamTableBase; // 参数大小表(每个服务的参数字节数)
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
  • ServiceTableBase系统服务函数表的基地址。

    • 在 x86 中,它是一个 函数地址数组,每个 DWORD 为一个系统调用的实现地址;

      1
      FuncAddr = KeServiceDescriptorTable.ServiceTableBase[index];
    • 在 x64 中,它是一个 偏移值数组,每项存储的是 (TargetFunctionAddress - KiSystemCall64Base) << 4

      1
      2
      FuncAddr = ((KeServiceDescriptorTable.ServiceTableBase[index] >> 4)
      + KeServiceDescriptorTable.ServiceTableBase);
  • ServiceCounterTableBase服务调用计数表的基地址。每个服务对应一个计数器,用于记录该系统服务被调用的次数。此字段通常为 NULL,仅在调试版本的内核中启用。

  • NumberOfServices系统调用的总个数,即 ServiceTableBase 中可用的函数总数。系统调用前会验证调用号是否越界(EAX/RAX 是否 < 该值)

  • ParamTableBase:参数长度表,每项是一个字节,表示对应系统服务调用的参数总大小(单位:字节)。

Windows 系统维护一个或多个服务表(内核表、GUI 表等),统一管理在如下符号变量中:

1
2
KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow[4];
  • KeServiceDescriptorTable 描述的是 ntoskrnl 表(native API),处理 Nt* 系列系统调用。KeServiceDescriptorTable 在低版本系统中是对外导出的。
  • KeServiceDescriptorTableShadow 是一个 KSERVICE_TABLE_DESCRIPTOR 结构体数组,有 4 项,但通常只使用前 2 项。KeServiceDescriptorTableShadow 不对外导出,但是通常位于 KeServiceDescriptorTable 表上方偏移 0x40 处。
    • KeServiceDescriptorTableShadow[0]KeServiceDescriptorTable 内容相同。
    • KeServiceDescriptorTableShadow[1] 描述的是 GUI/GDI(win32k.sys)服务 SSDT,处理 GUI 调用(如 NtUser*NtGdi*

KTHREAD.ServiceTable 会指向其中一个表,具体是哪个取决于当前线程是否是 UI 线程。

首先 Windows 会根据传入的系统调用号 EAX 提取服务表信息服务号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
;
; 输入寄存器说明:
; (eax) = 系统调用号(包含服务表信息 + 服务号)
; (edx) = 调用者的参数栈地址(用户栈指针)
; (esi) = 当前线程 (_KTHREAD) 结构地址
;
; 所有其他寄存器已保存,此函数可自由使用
;

_KiSystemServiceRepeat:
mov edi, eax ; 备份 EAX(系统调用号)

shr edi, SERVICE_TABLE_SHIFT ; 提取服务表标志(默认右移8位,获取 bit8~11)
and edi, SERVICE_TABLE_MASK ; 保留表选择位(0x00 或 0x10),用于构造偏移

mov ecx, edi ; 将服务表标志保存在 ECX(后面判断 GUI 表用)
add edi, [esi]+ThServiceTable ; edi = 当前线程的 ServiceTableBase + 表偏移(0 或 0x10)
; → 得到 KSERVICE_TABLE_DESCRIPTOR 结构地址

mov ebx, eax ; 备份原始 syscall 号(后续恢复)
and eax, SERVICE_NUMBER_MASK ; 提取 syscall 号低12位作为服务号(表内函数索引)

查询 SSDT 表的系统调用号 EAX 不只是一个简单的索引,而是由以下部分组成:

1
2
EAX (32-bit syscall number):
[ unused (18 bits) ][ TableOffset (2 bits) ][ ServiceNumber (12 bits) ]

即前 12 比特表示的是系统调用处理函数在 SSDT 表中的下标,而之后的 2 比特表示的是 SSDT 表在 KeServiceDescriptorTableShadowKTHREAD.ServiceTable)中的索引。

这段代码的逻辑是:

  • ECX = (EAX >> 8) & 0x10,即将 EDI 赋值为调用号对应在 KeServiceDescriptorTableShadow 中的偏移。这里右移 8 实际上就是在右移 12 的基础上乘上了 KSERVICE_TABLE_DESCRIPTOR 的大小 0x10,而 & 0x10 是为了清除服务号的高 4 比特且 KeServiceDescriptorTableShadow 只有前 2 项有效。
  • EDI = ECX + KTHREAD.ServiceTable,将 EDI 指向对应的 KSERVICE_TABLE_DESCRIPTOR 结构。
  • EBX = EAX,将原始的调用号备份到 EBX 中。
  • EAX = EAX & 0xFFF,取调用号的低 12 位得到服务号

之后会根据 KSERVICE_TABLE_DESCRIPTOR.NumberOfServices 检查服务号范围是否合法。

1
2
3
4
5
6
;
; 检查服务号是否在表的有效范围内(超过最大服务数则非法)
;

cmp eax, [edi]+SdLimit ; 比较服务号是否超出 NumberOfServices
jae Kss_ErrorHandler ; 如果越界,跳转错误处理(可能切换为 GUI 线程)

如果是 GUI 服务,并且线程的 GDI 批调用计数不为 0,则调用 KeGdiFlushUserBatch 将批量 GDI 操作刷新提交;这一步是防止内核处理一个系统调用时,用户空间的批处理操作尚未同步,确保图形一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;
; 如果当前表是 GUI 表(win32k.sys),并且存在 batched GDI 调用,则刷新批处理
;

cmp ecx, SERVICE_TABLE_TEST ; 检查是否是 GUI 表(== 0x10)
jne short Kss40 ; 如果不是 GUI 表,跳过刷新逻辑

mov ecx, PCR[PcTeb] ; 获取当前线程的 TEB(Thread Environment Block)地址
xor ebx, ebx ; 清零 ebx,准备检查 GDI 批处理数

KiSystemServiceAccessTeb:
or ebx, [ecx]+TbGdiBatchCount ; 判断 TEB 中是否有 GDI 批调用(可能触发页错误)

jz short Kss40 ; 如果为0,则没有 GDI 批处理,跳过

push edx ; 保存用户参数地址
push eax ; 保存 syscall 编号
call [_KeGdiFlushUserBatch] ; 调用内核函数刷新 GDI 批处理
pop eax ; 恢复 syscall 编号
pop edx ; 恢复用户参数地址

之后会更新服务调用计数,用于调试分析和性能统计。

  • KPCR.PrcbData.KeSystemCalls++,增加当前处理器的系统调用计数。
  • KSERVICE_TABLE_DESCRIPTOR.ServiceCounterTableBase[EAX]++,更新 SSDT 表中的服务调用次数,仅调试版中启用。
1
2
3
4
5
6
7
8
9
Kss40:
inc dword ptr PCR[PcPrcbData+PbSystemCalls] ; 增加当前处理器的系统调用计数

if DBG
mov ecx, [edi]+SdCount ; 获取调试用的调用计数表
jecxz short @f ; 如果未定义,跳过;jecxz 表示如果 ECX = 0,则跳转到 label 处执行
inc dword ptr [ecx+eax*4] ; 当前服务号的调用次数 +1
@@:
endif

@@@fMASM 汇编语法中的临时标签:

  • @@:表示一个临时标签(label),通常配合 @f@b 使用。
  • @f:表示“向前(forward)跳转”到最近定义的 @@
  • @b:表示“向后(backward)跳转”到最近定义的 @@

服务函数的参数总是从用户栈复制到内核栈,参数长度由 SdNumber 表给出(一个字节表示每项服务的参数总大小),复制前栈空间预分配,且防止从非法内核地址读取数据。最后调用对应服务号的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
;
; 将用户栈上的参数复制到内核栈
; 所有服务都这样做(即使参数为0),因为调用帧空间已提前分配
;

FPOFRAME ?FpoValue, 0 ; 调试器用:设置帧指针省略优化(可忽略)

mov esi, edx ; esi → 用户参数地址
mov ebx, [edi]+SdNumber ; ebx → 参数长度表地址
xor ecx, ecx
mov cl, byte ptr [ebx+eax] ; ecx = 参数长度(单位:字节)
mov edi, [edi]+SdBase ; edi → SSDT 函数表地址
mov ebx, [edi+eax*4] ; ebx = 对应服务号的函数地址

sub esp, ecx ; 为参数在内核栈中分配空间
shr ecx, 2 ; 将参数字节数转换为 DWORD 数(用于 rep movsd)
mov edi, esp ; edi → 参数目标地址(即内核栈顶)

cmp esi, _MmUserProbeAddress ; 检查参数地址是否为用户空间
jae kss80 ; 如果不是用户地址,跳过复制(已信任)

KiSystemServiceCopyArguments:
rep movsd ; 将用户参数从用户栈复制到内核栈(ecx 个 DWORD)

;
; (仅调试版)判断是否需要模拟低资源内存环境,触发内存修剪
;

if DBG
test _MmInjectUserInpageErrors, 2
jz short @f
stdCall _MmTrimProcessMemory, <0> ; 主动回收内存
jmp short kssdoit
@@:
mov eax, PCR[PcPrcbData+PbCurrentThread]
mov eax, [eax]+ThApcState+AsProcess
test dword ptr [eax]+PrFlags, 0100000h ; 是否为“页错误测试进程”
je short @f
stdCall _MmTrimProcessMemory, <0> ; 是 → 进行内存压缩
@@:
endif

;
; 正式调用系统服务函数(ebx → 函数地址,参数已在栈上就绪)
;

kssdoit:
call ebx ; 调用实际的系统服务函数

返回用户态

调用完 SSDT 表中对应的函数后就会返回用户态。在返回用户态的过程中会有一些列的检查。

首先会检查 IRQL 等级。如果是来自用户态的系统调用则需要确保当前 IRQL 等级为 0(PASSIVE_LEVEL),否则就跳转到 kss100 调用 KeBugCheck2 蓝屏报错。因为如果 IRQL 大于 0 则意味着当前中断上下文尚未清理完,系统不允许直接返回用户态。

IRQL(Interrupt Request Level,中断请求级别)是 Windows 内核中的一种中断优先级机制,用来 控制线程调度、中断响应和同步规则。Windows 内核会根据IRQL 的高低,来决定当前 CPU 允许处理哪些中断、调度哪些线程,以及能否执行某些操作。

常见的 IRQL 等级如下:

名称 说明
PASSIVE_LEVEL 0 普通线程运行级别(几乎所有代码运行在此)
APC_LEVEL 1 异步过程调用(APC)处理级别
DISPATCH_LEVEL 2 调度相关(如 DPC)运行在此级别
>=331 用于硬件中断服务例程(ISR)处理,不同设备映射到不同级别
HIGH_LEVEL 31 系统最高级别(屏蔽所有中断),只允许极少代码运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kss60:
;
; 检查是否从用户模式(Ring 3)返回时,当前中断级别 IRQL 不为 PASSIVE_LEVEL。
; 如果是这种情况,这是一个严重的逻辑错误,因为系统只能在 PASSIVE_LEVEL(0)
; 时才能安全地返回到用户模式。
;

if DBG

test byte ptr [ebp]+TsSegCs, MODE_MASK ; 检查当前陷阱帧中的 CS 段寄存器
; 的最低位(MODE_MASK = 1)。
; 如果结果为 1,表示从用户模式触发中断。

jz short kss50b ; 如果是 0(来自内核态),跳过后续检查。

mov esi, eax ; 暂存系统调用的返回值。

CurrentIrql ; 读取当前中断请求级别(IRQL)到 AL。
; 实际上这是一个宏,会读取 TPR 寄存器
; 或 CR8(在 x64 上)来获取当前 CPU IRQL。

or al, al ; 检查 AL 是否为 0(PASSIVE_LEVEL)

jnz kss100 ; 如果当前 IRQL > 0,则非法:
; 在高 IRQL 下不能返回到用户态,
; 跳转到 kss100 执行 KeBugCheck2 报错。

mov eax, esi ; 恢复返回值。

kss100 的蓝屏代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kss100:
push PCR[PcIrql] ; 将当前 IRQL 压栈,用作调试用的虚假参数

?FpoValue = ?FpoValue + 1 ; 更新 FPO(Frame Pointer Omission)帧编号

FPOFRAME ?FpoValue, 0
mov byte ptr PCR[PcIrql], 0 ; 重置当前 IRQL,避免递归陷入调试陷阱
cli ; 关闭中断,防止调试过程中被打断

;
; 错误:尝试在中断优先级大于 0 的情况下返回到用户模式。
; 将触发一个严重系统错误 IRQL_GT_ZERO_AT_SYSTEM_SERVICE。
;
; KeBugCheck2(
; IRQL_GT_ZERO_AT_SYSTEM_SERVICE, // 错误码
; ebx, // 系统调用处理函数地址
; eax, // 当前 IRQL
; 0,
; 0,
; ebp); // 陷阱帧地址
;

stdCall _KeBugCheck2, <IRQL_GT_ZERO_AT_SYSTEM_SERVICE, ebx, eax, 0, 0, ebp>

之后检查当前线程是否处于一种不允许从内核返回到用户模式的状态。如果发现:

  • 线程附加到其他进程(如 KeStackAttachProcess 创建的附加上下文);
  • 当前线程禁用了 APC 调度机制(即禁止内核异步过程调用)。

直接触发蓝屏(kss120,防止继续错误地返回到用户态。

APC(Asynchronous Procedure Call) 是 Windows 内核提供的一种机制,允许内核或用户模式的函数异步地在指定线程上下文中执行。它分为两种:

  • 用户模式 APC:线程即将从内核返回用户态时,由内核触发挂起的用户 APC 回调函数。
  • 内核模式 APC:在内核中异步执行某些回调(例如异步 I/O 完成通知)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    ;
; 检查是否禁止了内核 APC,或线程附加到其他进程(如通过 KeStackAttach)
; 如果存在这类非法状态,不能返回到用户模式,否则将导致系统不稳定。
;

mov ecx, PCR[PcPrcbData+PbCurrentThread] ; 获取当前处理器控制块中的当前线程指针(_KTHREAD)

mov dl, [ecx]+ThApcStateIndex ; 获取 APC 状态索引(用于判断线程是否 attach 到其他进程)

or dl, dl ; 判断 APC 状态索引是否为 0
jne kss120 ; 如果不为 0,说明线程处于 APC attach 状态,跳转执行 bugcheck(蓝屏)

mov edx, [ecx]+ThCombinedApcDisable ; 获取 APC 禁止计数器(包括用户和内核 APC)

or edx, edx ; 判断是否有 APC 被禁止
jne kss120 ; 如果禁止 APC,说明不能切回用户态,跳转报错处理

kss50b: ; 否则,检查通过,继续执行返回路径

在 Windows 内核中,内核 APC(Asynchronous Procedure Call)机制是安全返回用户态的重要保障

  • 如果 APC 被禁用(ApcDisable != 0),而线程又切回用户态,可能导致内核逻辑失效、死锁或内存破坏。
  • 如果线程附加到另一个进程上下文(ApcStateIndex != 0),说明该线程逻辑上“借用了”另一个进程的虚拟内存视图。这种情况下强行返回用户态会让线程执行在错误的进程地址空间,是非常严重的 bug

kss120 蓝屏代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;
; APC_INDEX_MISMATCH - 尝试从内核态返回到用户态时,发现当前线程处于非法状态:
; 要么是线程附加到了其他进程(KeStackAttachProcess),
; 要么是内核 APC(Asynchronous Procedure Call)被禁用了。
;
; 因此调用 KeBugCheck2 触发蓝屏(BUGCHECK 0x01)。
;
; 参数含义如下:
; KeBugCheck2(
; APC_INDEX_MISMATCH, ; bug 检查码:0x00000001
; ebx, ; 系统调用处理函数地址(system routine)
; eax = Thread->ApcStateIndex, ; 当前线程的 APC 状态索引
; edx = Thread->CombinedApcDisable, ; 当前线程的 APC 禁用计数器
; 0, ; 保留字段
; ebp = TrapFrame ; 当前陷阱帧(CPU 上下文)
; )

kss120:
movzx eax, byte ptr [ecx] + ThApcStateIndex
; 将线程结构中的 ApcStateIndex 读入 eax(用于判断是否被附加)

mov edx, [ecx] + ThCombinedApcDisable
; 将线程结构中的 CombinedApcDisable 读入 edx(用于判断是否禁用了内核 APC)

stdCall _KeBugCheck2, <APC_INDEX_MISMATCH, ebx, eax, edx, 0, ebp>
; 调用 KeBugCheck2 函数触发蓝屏,记录详细调试信息

之后清理堆栈,恢复之前复制参数时抬升的堆栈。

1
2
3
4
5
6
7
kss61:
;
; 服务例程返回路径,eax 中为状态码(返回值)。
; 此代码也可能是 KiCallbackReturn 调用失败后的恢复路径。
;

mov esp, ebp ; 清理栈空间(恢复服务调用前 ESP)

之后会从 TRAP_FRAME.Edx 恢复之前的 TRAP_FRAMEKTHREAD.TrapFrame,这一步是针对 INT 2E 指令对应的 _KiSystemService 函数进行的。而 SYSENER 指令对应的 _KiFastCallEntry 是直接返回 3 环,因此并不在乎 KTHREAD.TrapFrame 这个字段。

1
2
3
4
5
6
7
8
kss70:
;
; 恢复调用前的 Trap Frame 指针(供线程异常或中断时恢复上下文用)
;

mov ecx, PCR[PcPrcbData+PbCurrentThread] ; 获取当前线程地址
mov edx, [ebp].TsEdx ; 从当前 Trap Frame 中恢复旧 Trap Frame 指针
mov [ecx].ThTrapFrame, edx ; 设置回线程的 Trap Frame 域

之后会检查当前线程是否有挂起的用户 APC

  • 若有,则派发用户 APC 并再次检查;
  • 若无,则恢复 Trap Frame 中保存的用户态上下文,安全返回用户空间;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
_KiServiceExit proc

cli
; 禁用中断,防止上下文恢复过程被中断,保证返回路径一致性。

test byte ptr [ebp + KTRAP_FRAME.EFlags + 2], 2
; 检查 TrapFrame->EFlags.VM 位(bit 17),判断是否从 V86 模式返回。
; 如果是,则跳过普通用户态/内核态判断,直接处理 V86 相关逻辑。
jnz CheckApc

test byte ptr [ebp + KTRAP_FRAME.SegCs], 1
; 检查 CS 段选择子的最低位 RPL 是否为 1(用户态为 3,内核为 0)。
; 如果当前不来自用户模式,说明为嵌套系统调用返回,直接跳到恢复路径。
jz ExitToUser

CheckApc:
mov ebx, fs:KPCR.PrcbData.CurrentThread
; 获取当前线程 ETHREAD 指针,用于读取调度与 APC 状态字段。

test [ebx + KTHREAD.Header.ThreadControlFlags], 2
; 检查线程控制标志(ThreadControlFlags),是否启用了计数器跟踪(bit 1)。
; 如果开启,则需要调用 KiCopyCounters() 更新统计信息。
jz SkipCounters

push eax
push ebx
call _KiCopyCounters@4
pop eax

SkipCounters:
mov byte ptr [ebx + KTHREAD.Alerted], 0
; 清除线程的 Alerted 标志位,表示本次线程已完成 APC 检查。

cmp byte ptr [ebx + KTHREAD.ApcState.UserApcPending], 0
; 检查 ApcState 中的 UserApcPending 标志是否为真。
; 如果有用户态 APC 挂起,则需要先执行它们。
jz ExitToUser

; 以下准备恢复 Trap Frame 并执行 APC 调度逻辑
mov ebx, ebp
mov [ebx + KTRAP_FRAME.Eax], eax
; 将当前系统调用返回值 EAX 保存回 Trap Frame,防止 APC 覆盖。

; 设置 FS/DS/ES 为用户段,模拟从 Ring3 返回。
mov [ebx + KTRAP_FRAME.SegFs], KGDT_R3_TEB | RPL_MASK
mov [ebx + KTRAP_FRAME.SegDs], KGDT_R3_DATA | RPL_MASK
mov [ebx + KTRAP_FRAME.SegEs], KGDT_R3_DATA | RPL_MASK
mov [ebx + KTRAP_FRAME.SegGs], 0

mov ecx, APC_LEVEL
call ds:__imp_@KfRaiseIrql@4
; 提升 IRQL 至 APC_LEVEL,以屏蔽普通中断,保障 APC 派发的原子性。

push eax
sti
; 允许中断再次开启,使得内核 APC 能正常派发。

push ebx ; TrapFrame
push 0 ; kexcep_frame = NULL
push 1 ; PreviousMode = UserMode
call _KiDeliverApc@12

pop ecx
call ds:__imp_@KfLowerIrql@4
; 恢复原始 IRQL,重回调度点

mov eax, [ebx + KTRAP_FRAME.Eax]
; 恢复 EAX 寄存器的系统调用返回值。

cli
jmp CheckApc
; 再次检查是否有新 APC 被挂起(可能派发时新到达)。

在之后会恢复线程的 ExceptionListPreviousMode 以及调试寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
ExitToUser:
mov edx, [esp + KTRAP_FRAME.ExceptionList]
mov fs:KPCR.NtTib.ExceptionList, edx
; 恢复线程的 ExceptionList,确保 SEH 正常工作。

mov ecx, [esp + KTRAP_FRAME.PreviousPreviousMode]
mov esi, fs:KPCR.PrcbData.CurrentThread
mov byte ptr [esi + KTHREAD.PreviousMode], cl
; 恢复线程的 PreviousMode 标志,标识系统调用返回来源。

test [esp + KTRAP_FRAME.Dr7], 0FFFF23FFh
; 判断是否设置了调试寄存器(调试上下文需恢复)。
jnz RestoreDebugRegisters

RestoreDebugRegisters 就是把 TRAP_FRAME 中的调试寄存器恢复到寄存器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
; -------------------- 恢复调试寄存器 --------------------

RestoreDebugRegisters:
xor ebx, ebx
; 清空 ebx 用于临时寄存器。

mov esi, [ebp + KTRAP_FRAME.Dr0]
mov edi, [ebp + KTRAP_FRAME.Dr1]
mov dr7, ebx
; 清除调试使能标志(防止中间值触发调试异常)。

mov dr0, esi
mov ebx, [ebp + KTRAP_FRAME.Dr2]
mov dr1, edi
mov dr2, ebx
; 恢复 dr0 ~ dr2(断点地址寄存器)。

mov esi, [ebp + KTRAP_FRAME.Dr3]
mov edi, [ebp + KTRAP_FRAME.Dr6]
mov ebx, [ebp + KTRAP_FRAME.Dr7]
mov dr3, esi
mov dr6, edi
mov dr7, ebx
; 恢复 dr3、dr6、dr7,完成调试状态恢复。

jmp ContinueReturn
; 跳回系统调用返回路径。

最后就是根据调用者的状态分别调转到不同的返回逻辑中。如果是返回 3 环是通过 iretd 指令返回。这是因为内核在返回用户态的时候不清楚当初系统调用是通过 sysenter 还是 int 2E 完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
ContinueReturn:
test [esp + KTRAP_FRAME.EFlags], EFLAGS_V86_MASK
; 判断是否为 V86 模式返回。
jnz V86Return

test word ptr [esp + KTRAP_FRAME.SegCs], 0FFF8h
; 检查 CS 段选择子是否合法(是否为 NULL 段选择子)。
; 如果为 NULL(0x0000),说明 Trap Frame 不可靠,构造伪栈进行返回。
jz V86ReturnFromNullCs

cmp word ptr [esp + KTRAP_FRAME.SegCs], KGDT_R3_CODE | RPL_MASK
; 检查段值是否为用户态代码段(0x1B)
bt word ptr [esp + KTRAP_FRAME.SegCs], 0
cmc
ja KernelReturnFallback

cmp word ptr [ebp + KTRAP_FRAME.SegCs], KGDT_R0_CODE
jz SkipSegmentRestore

SkipSegmentRestoreWithFS:
lea esp, [ebp + KTRAP_FRAME.SegFs]
pop fs

SkipSegmentRestore:
lea esp, [ebp + KTRAP_FRAME.Edi]
pop edi
pop esi
pop ebx
pop ebp
; 恢复非易变寄存器。

cmp word ptr [esp + 8], 80h
; 判断是否为 V86 模式的段。
ja V86Fixup

add esp, 4
test dword ptr [esp + 4], MODE_MASK
; 判断是否返回至用户态。

KiSystemCallExitBranch:
jnz KiSystemCallExit
; 如果 Trap Frame 中指示目标是用户态(EFLAGS 中 MODE_MASK 为真),则跳转至用户态返回逻辑。

pop edx ; EIP
pop ecx ; CS
popfd ; EFLAGS
jmp edx
; 否则(内核态),弹出 EIP/CS/EFLAGS,并跳转至返回地址(edx)。

KiSystemCallExit:
iretd
; 使用 IRETD 返回用户空间,恢复 EIP、CS 和 EFLAGS。

; -------------------- V86 模式:段为 NULL 时的特殊返回 --------------------

V86ReturnFromNullCs:
movzx ebx, [esp + KTRAP_FRAME.TempSegCs]
mov [esp + KTRAP_FRAME.SegCs], ebx
; 使用保存的临时段选择符 TempSegCs 修复 SegCs 字段。

mov ebx, [esp + KTRAP_FRAME.TempEsp]
sub ebx, 0Ch
; 为伪造的 IRET 栈帧分配空间。

mov [esp + KTRAP_FRAME.ErrCode], ebx
; 暂存 ErrorCode 位置,作为 IRET 的栈基地址。

mov esi, [esp + KTRAP_FRAME.EFlags]
mov [ebx + 8], esi
mov esi, [esp + KTRAP_FRAME.SegCs]
mov [ebx + 4], esi
mov esi, [esp + KTRAP_FRAME.Eip]
mov [ebx], esi
; 构造仿真的用户栈帧(三元组:EIP, CS, EFLAGS)。

add esp, 54h
; 跳过 Trap Frame 内容。

pop edi
pop esi
pop ebx
pop ebp
mov esp, [esp]
; 将 esp 设置为仿真堆栈的指针。

iret
; 使用构造的用户空间 IRET 栈帧返回。

; -------------------- V86 模式常规返回 --------------------

V86Return:
add esp, 3Ch
; 跳过大部分 Trap Frame 区域。

pop edx
pop ecx
pop eax

lea esp, [ebp + 54h]
; 定位到栈顶尾部(除 Trap Frame 之外的保存区)。

pop edi
pop esi
pop ebx
pop ebp

cmp word ptr [esp + 8], 80h
; 检查 CS 是否属于虚拟 8086 模式段范围(如 0xF8xx)

ja V86Fixup
; 若是异常段值,进入修复逻辑。

add esp, 4
; 正常补齐 ExceptionCode 槽位。

iret
; 执行返回。

; -------------------- 修复非法虚拟 8086 模式堆栈 --------------------

V86Fixup:
cmp word ptr [esp + 2], 0
jz short V86PostFixup
; 高 16 位为 0,直接跳过

cmp word ptr [esp], 0
jnz short V86PostFixup
; 低 16 位为非 0,说明 SS = 0x0000F8,合法,跳过

shr dword ptr [esp], 10h
; 提取 SS 段值

mov word ptr [esp + 2], 0F8h
; 构造合规 SS = 0x00F8 段

lss sp, [esp]
; 加载 SS:SP 到当前栈

movzx esp, sp
; 移除高位扩展,确保 esp 为栈指针

iret
; 执行返回

V86PostFixup:
iret
; 修复完成后正常返回

; -------------------- 内核调用返回路径(不恢复 FS) --------------------

KernelReturnFallback:
mov eax, [esp + KTRAP_FRAME.Eax]
; 恢复返回值

add esp, 30h
; 跳过部分 Trap Frame 内容(Segment 寄存器及异常处理信息)

pop gs
pop es
pop ds
pop edx
pop ecx
; 恢复其他寄存器状态

jmp SkipSegmentRestoreWithFS
; 跳至跳过 FS 的恢复逻辑

_KiServiceExit endp

KiSystemService(INT 2E)

INT 2E 是旧式的系统调用方式,进入内核态后会跳转到 _KiSystemService。另外很多内核态的函数实际上也是通过调用 _KiSystemService 实现的。

提示

在内核中,Zw* 函数和 Nt* 函数在功能上是一致的,但是 Zw* 函数会经过 SSDT 表,因此效率低且会被一些 SSDT 相关的 Hook 监控。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
; NTSTATUS __stdcall ZwOpenProcess(
; OUT PHANDLE ProcessHandle,
; IN ACCESS_MASK DesiredAccess,
; IN POBJECT_ATTRIBUTES ObjectAttributes,
; IN PCLIENT_ID ClientId
; )

public _ZwOpenProcess@16
_ZwOpenProcess@16 proc near ; Zw 函数在 ntdll 或内核中的封装

ProcessHandle = dword ptr 4 ; 第 1 个参数:返回句柄
DesiredAccess = dword ptr 8 ; 第 2 个参数:访问权限
ObjectAttributes = dword ptr 0Ch ; 第 3 个参数:对象属性指针
ClientId = dword ptr 10h ; 第 4 个参数:客户端 ID 指针

; --- 设置系统调用号 ---
mov eax, 0BEh ; EAX = 0xBE,系统调用号,代表 ZwOpenProcess

; --- 设置参数地址 ---
lea edx, [esp+ProcessHandle] ; EDX = 参数地址

; --- 模拟 INT 调用 ---
pushf ; 模拟 INT 会自动压入的 EFLAGS
push 8 ; 模拟 INT 会自动压入的 CS = 0x8(KGDT_R0_CODE,ring 0 代码段选择子)

; --- 调用内核系统调用入口 ---
call _KiSystemService ; 实际执行系统调用,这里还会压入一个 EIP 构成完整的 INT 指令的压栈行为

; --- 函数返回 ---
retn 10h ; 平衡堆栈:4 个参数 × 4 字节 = 0x10
_ZwOpenProcess@16 endp

_KiSystemService 实现比 _KiFastCallEntry 要简洁一些,主要过程是先调用 ENTER_SYSCALL 宏构造 TRAP_FRAME 结构,之后跳转到 _KiFastCallEntry_KiSystemServiceRepeat 调用 SSDT 表中的处理函数然后返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;
; 通用系统服务入口点(由 INT 2E 调用)
;

PUBLIC _KiSystemService
_KiSystemService proc

ENTER_SYSCALL kss_a, kss_t ; 构造 Trap Frame、保存寄存器状态
; - 保存 EBP/EBX/ESI/EDI/FS
; - 设置当前线程的 TrapFrame 指针
; - 保存 ExceptionList / PreviousMode
; - 设置 FS = 内核 PCR
; - 清空方向位 CLD
; - 开启中断 STI
; 如果启用调试,还会处理调试上下文

jmp _KiSystemServiceRepeat ; 跳转到通用服务分发处理逻辑

_KiSystemService endp

由于对于 SYSENTER 指令 CPU 什么都没压,并且还跑在 per-CPU DPC Stack;内核 prolog 必须自己构造 IRET Frame、手动换到线程栈,再建完整 TrapFrame,因而步骤更多、字段也多预留了 NPX 区。

而对于 INT 2E 指令,CPU 已经帮忙切栈、压返回态;内核 prolog 只需微调——改 FS、存非易失寄存器、挂 TrapFrame。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
;++
;
; ENTER_SYSCALL AssistLabel, TargetLabel, NoFSLoad, RejectVdmLabel, SaveEcx
;
; 宏功能说明:
;
; 构造系统调用(syscall)进入时的 Trap Frame,并设置必要的寄存器。
;
; 会保存的内容:
; - 错误码填充(Errorpad,占位)
; - 非易失性寄存器(EBP、EBX、ESI、EDI)
; - FS 段寄存器
; - ExceptionList(异常处理链)
; - PreviousMode(线程的先前 CPU 模式)
;
; 不会保存:
; - 易失性寄存器(EAX、ECX、EDX)
; - 段寄存器(除了 FS)
; - 浮点状态
;
; 会设置:
; - FS 寄存器
; - ExceptionList
; - PreviousMode
; - 清除方向标志位(cld)
;
; 参数说明:
; AssistLabel - 用于调试的标签,会跳到调试辅助代码(如果调试启用)
; TargetLabel - 系统调用入口处的目标标签
; NoFSLoad - 若指定则不修改 FS,默认会设置 FS=KGDT_R0_PCR
; SaveEcx - 若指定则会额外保存 ECX 到 Trap Frame 中
;
; 出口状态:
; - 不改变中断状态(与进入前一致)
; - ESP 和 EBP 都指向新的 Trap Frame
; - 保持入口时的 EAX 和 EDX 不变
;
; 注意事项:
; - 若去除了 DS: 引用,需要明确加上段前缀覆盖
;
;--

ENTER_SYSCALL macro AssistLabel, TargetLabel, NoFSLoad, RejectVdmLabel, SaveEcx

.FPO ( FPO_LOCALS, FPO_PARAMS, FPO_PROLOG, FPO_REGS, FPO_USE_EBP, FPO_TRAPFRAME )

ifdef KERNELONLY

; ========== 开始构造 Trap Frame ==========

push 0 ; 占位,作为 error code pad
push ebp ; 保存非易失寄存器
push ebx
push esi
push edi

ifb <NoFSLoad>
push fs ; 保存 FS 寄存器,并设置为指向内核 PCR
mov ebx, KGDT_R0_PCR ; 加载内核全局描述符表中 PCR 段号
mov fs, bx
else
; 如果使用 fast system call 进入,FS 已经设为内核 PCR,不需重复设置
push KGDT_R3_TEB OR RPL_MASK ; 直接压入用户态 TEB 段值(用于返回)
endif

mov esi, PCR[PcPrcbData+PbCurrentThread] ; 获取当前线程结构指针

; -------- 保存异常处理链并初始化为空 --------

push PCR[PcExceptionList] ; 保存旧的 ExceptionList
mov PCR[PcExceptionList], EXCEPTION_CHAIN_END ; 清空异常处理链

; -------- 保存 PreviousMode 并更新为当前调用者的模式 --------

push [esi]+ThPreviousMode ; 保存线程的旧 PreviousMode
sub esp, TsPreviousPreviousMode ; 为 Trap Frame 分配剩余空间

mov ebx, [esp+TsSegCS] ; 取调用者的 CS 段寄存器
and ebx, MODE_MASK ; 提取最低位判断是否为用户模式
mov [esi]+ThPreviousMode, bl ; 更新线程中的 PreviousMode 字段

; -------- 设置新的 Trap Frame 地址并保存旧 Trap Frame 指针 --------

mov ebp, esp ; 设置当前 Trap Frame 基址
mov ebx, [esi].ThTrapFrame ; 获取线程中原 Trap Frame 指针
mov [ebp].TsEdx, ebx ; 写入 Trap Frame 中的 .Edx 字段,表示“旧 Trap Frame”

ifnb <SaveEcx>
mov [ebp].TsEcx, ecx ; 如果要求保存 ECX,则写入 Trap Frame
endif

and dword ptr [ebp].TsDr7, 0 ; 清空调试寄存器标志

test byte ptr [esi].ThDebugActive, 0FFh ; 若线程启用了调试
mov [esi].ThTrapFrame, ebp ; 将新的 Trap Frame 设置到线程结构中

cld ; 清除方向标志(确保字符串操作前向)

jnz Dr_&AssistLabel ; 若启用调试,跳转到调试处理分支

Dr_&TargetLabel: ; 调用系统服务的正式入口标签

SET_DEBUG_DATA ; 设置调试数据(此宏可能破坏 EDI)

sti ; 恢复中断允许状态

else
%out ENTER_SYSCAL outside of kernel
.err
endif
endm

_KiSystemServiceENTER_SYSCALL 在构造 TRAP_FRAME 时主要有如下不同点:

  • 栈切换与返回帧构造

    • _KiSystemService(INT 2E) :CPU 在执行 INT 2E 指令的时候已经完成了栈切换,并且压了 EIP/CS/EFLAGS/SS/ESP 因此直接从 ErrCode 开始构造。
    • _KiFastCallEntry(SYSENTER) :MSR 指定的是 DPC Stack(per-CPU);所以内核需要先切换内核堆栈,然后再在那里建“伪 IRET 帧”。
  • PreviousMode 判定逻辑

    • _KiSystemService(INT 2E) :根据来时 CS.RPL 动态判定,因为调用方既可能是用户态也可能是内核态。
    • _KiFastCallEntry(SYSENTER)SYSENTER 指令只能由用户态调用,因此直接强制设置为从用户态调用,另外还会在 KiFastCallEntry 开头“修复”用户态的段寄存器。
  • 嵌套保存 TRAP_FRAME

    • _KiSystemService(INT 2E) :会将上一个系统调用的 TRAP_FRAME 地址 KTHREAD.TrapFrame 保存到 TRAP_FRAME.Edx 然后再设置 KTHREAD.TrapFrame 指向当前 TRAP_FRAME

      这是为了支持系统调用的嵌套 / 递归(例如内核里再次发起 syscall),可通过 TrapFrame.TsEdx 字段找到上一个 Trap Frame,用于 unwind 或调试工具遍历调用链。

    • _KiFastCallEntry(SYSENTER)KiFastCallEntry 只会设置设置 KTHREAD.TrapFrame 指向当前 TRAP_FRAME,而不将上一个系统调用的 TRAP_FRAME 地址 KTHREAD.TrapFrame 保存到 TRAP_FRAME.Edx

      主要因为 SYSENTER 是为 纯用户态到内核 的快速单向路径设计的,性能优先,不考虑内核中间层再发起 syscall 的情形。

SSDT Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <ntddk.h>

// ========== 类型定义 ==========

// 64 位下 SSDT 表项是 PULONG_PTR(即 PVOID*),不是 PULONG
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PULONG_PTR ServiceTableBase; // 系统服务函数地址表(SSDT)
PULONG ServiceCounterTableBase; // 调用次数计数表(可为 NULL)
ULONG_PTR NumberOfServices; // 系统服务数量
PUCHAR ParamTableBase; // 参数长度表(64 位中已无意义)
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

// 导入 SSDT 结构(老系统导出,现代系统可用符号解析等方式获取)
__declspec(dllimport) KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;

// SSDT 中 NtOpenProcess 的索引(Win7 x64 一般为 0xBE,建议动态获取)
#define SSDT_INDEX_NTOPENPROCESS 0xBE

// 函数指针定义:NtOpenProcess 原型
typedef NTSTATUS(*FnNtOpenProcess)(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
);

// ========== 全局变量 ==========

static FnNtOpenProcess g_OriginalNtOpenProcess = NULL;
static volatile LONG g_ActiveCallCount = 0;


// ========== 工具函数 ==========

// 睡眠指定毫秒(用于卸载等待)
VOID SleepMilliseconds(ULONG ms)
{
LARGE_INTEGER interval;
interval.QuadPart = -(10 * 1000 * (LONGLONG)ms); // 单位:100ns
KeDelayExecutionThread(KernelMode, FALSE, &interval);
}

// 将内存映射为可写(使用 MDL)
PVOID MapMemoryAsWritable(PVOID address, SIZE_T size, PMDL* outMdl)
{
PMDL mdl = IoAllocateMdl(address, (ULONG)size, FALSE, FALSE, NULL);
if (!mdl) return NULL;

MmBuildMdlForNonPagedPool(mdl);

PVOID mapped = MmMapLockedPagesSpecifyCache(
mdl, KernelMode, MmNonCached, NULL, FALSE, NormalPagePriority
);

if (!mapped)
{
IoFreeMdl(mdl);
return NULL;
}

if (outMdl) *outMdl = mdl;
return mapped;
}

// 解除映射并释放 MDL
VOID UnmapMemory(PVOID mapped, PMDL mdl)
{
if (mapped && mdl)
{
MmUnmapLockedPages(mapped, mdl);
IoFreeMdl(mdl);
}
}


// ========== Hook 函数 ==========

NTSTATUS HookedNtOpenProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
)
{
InterlockedIncrement(&g_ActiveCallCount);

DbgPrint("[SSDT Hook] NtOpenProcess called for PID: %u\n",
(ULONG)(ULONG_PTR)ClientId->UniqueProcess);

NTSTATUS status = g_OriginalNtOpenProcess(
ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);

InterlockedDecrement(&g_ActiveCallCount);
return status;
}


// ========== 驱动入口 ==========

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;

// 获取 SSDT 表地址
PULONG_PTR ssdt = KeServiceDescriptorTable.ServiceTableBase;

// 映射为可写
PMDL mdl = NULL;
PULONG_PTR mappedSSDT = (PULONG_PTR)MapMemoryAsWritable(ssdt, PAGE_SIZE, &mdl);
if (!mappedSSDT) return STATUS_UNSUCCESSFUL;

// 保存原函数指针并 Hook
g_OriginalNtOpenProcess = (FnNtOpenProcess)mappedSSDT[SSDT_INDEX_NTOPENPROCESS];
mappedSSDT[SSDT_INDEX_NTOPENPROCESS] = (ULONG_PTR)HookedNtOpenProcess;

UnmapMemory(mappedSSDT, mdl);
DbgPrint("[SSDT Hook] Hook installed at index 0x%X\n", SSDT_INDEX_NTOPENPROCESS);

return STATUS_SUCCESS;
}


// ========== 驱动卸载 ==========

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);

// 获取 SSDT 表地址
PULONG_PTR ssdt = KeServiceDescriptorTable.ServiceTableBase;

// 映射为可写
PMDL mdl = NULL;
PULONG_PTR mappedSSDT = (PULONG_PTR)MapMemoryAsWritable(ssdt, PAGE_SIZE, &mdl);
if (!mappedSSDT) return;

// 恢复原函数指针
mappedSSDT[SSDT_INDEX_NTOPENPROCESS] = (ULONG_PTR)g_OriginalNtOpenProcess;

UnmapMemory(mappedSSDT, mdl);
DbgPrint("[SSDT Hook] Hook removed. Waiting for active calls...\n");

// 等待所有挂钩函数返回
while (InterlockedCompareExchange(&g_ActiveCallCount, 0, 0) != 0)
SleepMilliseconds(10);

SleepMilliseconds(300); // 额外安全延迟
DbgPrint("[SSDT Hook] Driver unloaded safely.\n");
}
  • Title: windows 系统调用
  • Author: sky123
  • Created at : 2022-09-28 11:45:14
  • Updated at : 2025-06-30 17:12:37
  • Link: https://skyi23.github.io/2022/09/28/windows 系统调用/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments